#! /usr/bin/python

############### L I C E N C E ################
# This is BSD licenced software, and as such:
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# Or put in the laymans words: "You use, your responsibility."
#
# Copyright (C) H. Lindqvist 2005, 2006, 2007.
#


################# H E L P ###################
# There is good documentation to be found in the program's configuration
# file (mkcomplinks.ini).
#


########### I N F O R M A T I O N ############
# This script creates links from a target subtree
# to a given directory's files. The idea is shamelessy
# stolen from the ACC (a computer club) and a set of
# scripts used "over there" to handle programs compiled
# for multiple platforms.
#
# Like the (ancient) UNIX pun on MULTICS, this is then
# a unary version of that multi platform version =)
#


############# E D I T   T H I S ###############
#
configuration = '/path/to/your/mkcomplinks.ini'
#
############# ^^^^^^^^^^^^^^^^^ ###############


############## I N C L U D E S ###############
import ConfigParser, getopt, sys, os, os.path


############## D E F A U L T S ################

# Colors used for printing
color            = {}
color['INFO']    = '\033[0;1m'   # Bold white
color['ERROR']   = '\033[31m'	# Red
color['SUCCESS'] = '\033[36m'	# Cyan
color['normal']  = '\033[0m'	 # Restore default


############ C O N T R O L L E R S ############
# Do not edit these (see the configuration file). 
# They are here for sake of scope.
force	   = False
delemptydir = False
staleonly   = False
verbose	 = False


############ S T A T I S T I C S ##############
madedirs	 = 0
madelinks	= 0
removeddirs  = 0
removedlinks = 0


############# F U N C T I O N S ################

#
# Prints help message
def help():
	print 'Usage: ' + sys.argv[0] + ' [-c] [-f] [-h] [-p <file>] [-r] [-v]'
	print """
		-c : Turn off colorful output (disables use of ANSI terminal codes)
		-f : Force removal of conflicting symlinks during linking
		-r : Clean up target subtree from stale symlinks and exit. If specified
			 any other requested actions (linking) will be ignored. Empty
			 directories will be removed as well.
		-p : Read configuration properties from the specified file.
		-v : Turn on verbose mode
	"""
#
# Print messages, using colors if requested
def message(type, mess):
	try:
		print color[type] + type + color['normal'] + ': ' + mess
	except KeyError:
		print type + ': ' + mess

#
# Links in the subtree from 'current' into the subtree 'target'
# using symlinks for files and creates missing catalogues as
# is needed
def linktargets(current, target):
	# From 'Statistics' section above
	global madedirs, madelinks, removedlinks

	if os.path.isdir(current):
		if not os.path.isdir(target):
			# Make target dir
			try:
				os.makedirs(target)
				madedirs += 1

				if verbose:
					message('INFO', 'Make dir ' + target)
			except OSError, (errno, strerror):
				# Directory already exists
				if errno != 17:
					message('ERROR', 'Failed to create directory: ' + target)
					message('ERROR', 'Please check permissions')
				# Some other error.
				else:
					message('ERROR', strerror)

				sys.exit(1)

		# Recurse for each of the subdirs
		try:
			for x in os.listdir(current):
				linktargets(current + '/' + x, target + '/' + x)
		except OSError, (errno, strerror):
			message('ERROR', 'Failed to list directory: ' + current)
			message('ERROR', 'Failure was due to the error: ' + strerror)
			sys.exit(1)

	# Link files
	else:
		# If the target is a symlink and the force flag (-f) was given,
		# attempt to unlink the target before linking
		if os.path.islink(target) and force:
			try:
				os.unlink(target)
				removedlinks += 1

				if verbose:
					message('INFO', 'Remove symlink: ' + target)
			except OSError, (errno, strerror):
				message('ERROR', 'Link already exists: ' + target)
				message('ERROR', 'The link could not be unlinked due to the error: ' + strerror)
				sys.exit(1)

		try:
			os.symlink(current, target)
			madelinks += 1

			if verbose:
				message('INFO', 'Symlink ' + target + '\n\t--> ' + current)
		except OSError, (errno, strerror):
			# Link already exists
			if errno == 17:
				message('ERROR', 'File already exists: ' + target)
				message('ERROR', 'Rerun with \'-f\' to force overwrite of symlinks,')
				message('ERROR', 'or rerun with \'-r\' to remove stale links')
			# Some other error, what?
			else:
				message('ERROR', strerror)

			sys.exit(1)


#
# Checks if a link is of wrong version (as is defined in the ini-file).
# As a helper function to 'removestale' it _assumes_ that the link argument
# should beremoved if it is deemed that is is not of any currently valid verion
# as has been defined in the configuration file.
def wrongversion(link, programs):
	# If it isn't a link, we should ignore it.
	if not os.path.islink(link):
		return false

	# Check if any portion of the link's pointing value is present in the 
	# configuration, indicating that the link should remain.
	for x in programs:
		if os.readlink(link).find(x) != -1:
			return False

	# No correct version was found, it should be removed.
	return True

#
# Removes stale symlinks from the 'target' subtree
def removestale(subtree, programs):
	# From 'Statistics' section above
	global removeddirs, removedlinks

	try:
		for x in os.listdir(subtree):
			f = subtree + '/' + x

			if os.path.isdir(f):
				removestale(f, programs)

				# Remove the sub directory if it is empty
				if delemptydir and not os.listdir(f):
					try:
						os.rmdir(f)
						removeddirs +=1

						if verbose:
							message('INFO', 'Removed empty directory: ' + f)
					except OSError, (errno, strerror):
						message('ERROR', 'Failed to remove empty directory')
						message('ERROR', 'Failure was due to the error: ' + strerror)

			elif os.path.islink(f) and ( not os.path.isfile(f) or wrongversion(f, programs) ):
				try:
					os.unlink(f)
					removedlinks += 1

					if verbose:
						message('INFO', 'Removed: ' + f)
				except OSError, (errno, strerror):
					message('ERROR', 'Failed to remove stale symlink')
					message('ERROR', 'Failure was due to the error: ' + strerror)
	except OSError, (error, strerror):
		message('ERROR', 'Failed to list directory: ' + subtree)
		message('ERROR', 'Failure was due to the error: ' + strerror)
		sys.exit(1)

#
# Dispalys some statistics
def printstat():
	message('INFO', '------------------------------------')
	message('INFO', 'Number of new directories:        ' + str(madedirs))
	message('INFO', 'Number of created symlinks:       ' + str(madelinks))
	message('INFO', 'Number of removed directories:    ' + str(removeddirs))
	message('INFO', 'Number of removed/replaced links: ' + str(removedlinks) + '\n')

################# B E G I N ###################
#
# The starting point.
def main():
	global configuration, delemptydir, force, staleonly, verbose

	# Parse paramters.
	try:
		opts, args = getopt.getopt(sys.argv[1:], 'cfhp:rv')
	except getopt.GetoptError:
		help()
		sys.exit(2)
	
	for o, a in opts:
		if o == '-c':
			color = {} # Empty color setting
		elif o == '-f':
			if verbose:
				message('INFO', '\'-f\' specified, will remove existing conflicting symlinks')
			force = True
		elif o == '-h':
			help()
			sys.exit(0)
		elif o == '-p':
			# Change the setting of the configuration file to use
			configuration = a
		elif o == '-r':
			message('INFO', '\'-r\' specified, cleaning up stale links only')
			staleonly = True
		elif o == '-v':
			verbose = True
		else:
			message('ERROR', 'Bad parameter: ' + o)
			help()
			sys.exit(1)

	
	# Load in configuration
	if not os.path.isfile(configuration):
		message('ERROR', 'No such configuration file: \'' + configuration + '\'')
		sys.exit(1)
	config = ConfigParser.ConfigParser()
	config.read(configuration)
	
	try:
		# If not activated on command line, check if they should be now 
		# per configuration.
		if not force:
			force = ( 'True' == config.get('Settings', 'force') )
		
		if not verbose:
			verbose = ( 'True' == config.get('Settings', 'verbose') )

		# This setting should be used with some thought, hence it can only
		# be altered through the configuration file where approriate comments
		# are available.
		if not delemptydir:
			delemptydir = ( 'True' == config.get('Settings', 'delemptydir') )
	
		# Linking settings.
		destdir   = config.get('Linking', 'destdir')
		sourcedir = config.get('Linking', 'sourcedir')
		subtarget = config.get('Linking', 'subtarget').split()
		programs  = config.get('Linking', 'programs').split()
	except ConfigParser.NoSectionError, detail:
		message('ERROR', str(detail))
		sys.exit(1)
	except ConfigParser.NoOptionError, detail:
		message('ERROR', str(detail))
		sys.exit(1)
	
	
	# Clean up linking directory and exit.
	if staleonly:
		removestale(destdir, programs)
		if verbose:
			printstat()
		sys.exit(0)
	
	# Make sure that 'directory' refers to an actual directory
	if not os.path.isdir(sourcedir):
		message('ERROR', 'Not a directory - \'' + sourcedir + '\'')
		sys.exit(1)
	else:
		if verbose:
			message('INFO', 'Linking contents of \'' + sourcedir)
	
	# Get fully qualified path, with any '.' and '..' removed
	p = os.path.realpath(sourcedir)
	
	# Link in subtree
	try:
		for x in programs:
			for y in os.listdir(p + '/' + x):
				if y in subtarget:
					linktargets(p + '/' + x + '/' + y, destdir + '/'+ y)
	except OSError, (errno, strerror):
		message('ERROR', 'Failed to list source subtree')
		message('ERROR', 'Failure was due to the error: ' + strerror)
		sys.exit(1)
	
	if verbose:
		printstat()
	
	message('SUCCESS', 'Linking successful. Terminating.')

if __name__ == '__main__':
	main()

